package edu.uvic.TabViz.framework.primitives;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;

import android.graphics.Color;
import android.opengl.GLES20;
import android.opengl.GLU;
import android.opengl.Matrix;

public class Object3D {

	protected int indexSize,VertexSize;
	protected String name;

	protected FloatBuffer vertexBuffer,normalBuffer,colorBuffer,textureBuffer;
	protected ShortBuffer indexBuffer;

	protected ArrayList<Object3D> objectNodes;
	int objectNodeCount = 0;

	public Point3D initPoint;
	protected Point3D position, rotation, scale;
	protected Material material;
	protected Light light;

	protected boolean visible = true;
	protected boolean transparent = false;
	protected int GL_Mode = GLES20.GL_TRIANGLES;

	protected float[] modelViewMatrix = new float[16];
	protected float[] projectionMatrix = new float[16];
	public float[] accRotation = new float[16];
	public float[] accRotationInit = new float[16];
	private boolean touch = false;

	public Object3D(){
		objectNodes = new ArrayList<Object3D>();
		position = new Point3D();
		rotation = new Point3D();
		scale = new Point3D(1, 1, 1);
		Matrix.setIdentityM(accRotation, 0);
		Matrix.setIdentityM(accRotationInit, 0);
	}

	public Object3D(String name) {
		this();
		this.name = name;
	}

	public Object3D(Object3DSerialized o3Ds){
		this();
		position = new Point3D(o3Ds.position);
		rotation = new Point3D(o3Ds.rotation);
		initPoint = new Point3D(o3Ds.position);
		setData(o3Ds.vertices, o3Ds.normals, o3Ds.colors, o3Ds.textures, o3Ds.indices);
		this.transparent = o3Ds.transparent;
		this.visible = o3Ds.visible;
		this.touch = o3Ds.touch;
		if (scale != null)
			scale = new Point3D(o3Ds.scale);
		if (o3Ds.light != null)
			this.light = new Light(o3Ds.light);
		if (o3Ds.material != null)
			material = new Material();
		this.accRotation = o3Ds.accRotation;
		this.accRotationInit = o3Ds.accRotation;
		objectNodes = new ArrayList<Object3D>();
		for (Object3DSerialized o : o3Ds.getObjects())
			objectNodes.add(new Object3D(o));
	}

	public void setData(float[] vertices, float[] normals, float[] colors, float[] textures, short[] indices) {
		vertexBuffer = ByteBuffer
				.allocateDirect(vertices.length * 4)
				.order(ByteOrder.nativeOrder()).asFloatBuffer();
		vertexBuffer.put(vertices).position(0);

		normalBuffer = ByteBuffer.allocateDirect(normals.length * 4)
				.order(ByteOrder.nativeOrder()).asFloatBuffer();
		normalBuffer.put(normals).position(0);

		textureBuffer = ByteBuffer.allocateDirect(textures.length * 4)
				.order(ByteOrder.nativeOrder()).asFloatBuffer();
		textureBuffer.put(textures).position(0);

		if (colors != null) {
			colorBuffer = ByteBuffer
					.allocateDirect(colors.length * 4)
					.order(ByteOrder.nativeOrder()).asFloatBuffer();
			colorBuffer.put(colors).position(0);
		} else {
			setColor(0xff000000 + (int)(Math.random() * 0xffffff));
		}

		indexBuffer = ByteBuffer.allocateDirect(indices.length * 2)
				.order(ByteOrder.nativeOrder()).asShortBuffer();
		indexBuffer.put(indices).position(0);
		indexSize = indices.length;
		VertexSize = vertices.length/3;
	}

	public void addNode(Object3D object3D){
		objectNodes.add(object3D);
	}

	public Object3D getNode(int index){
		return objectNodes.get(index);
	}

	public Object3D getNode(String name){
		for (int i=0;i<objectNodes.size();i++)
			if(objectNodes.get(i).getName().equalsIgnoreCase(name))
				return objectNodes.get(i);
		return null;
	}

	public void render(Camera camera) {
		render(camera, null);
	}

	public void render(Camera camera, final float[] parentMatrix){
		if(!visible) return;
		projectionMatrix = camera.getProjectionMatrix();
		if (!touch){
			GLES20.glEnable(GLES20.GL_CULL_FACE);
			if (transparent) {
				GLES20.glDisable(GLES20.GL_DEPTH_TEST);
				GLES20.glEnable(GLES20.GL_BLEND);
				GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
				GLES20.glDepthMask(false);
			} else {
				GLES20.glEnable(GLES20.GL_DEPTH_TEST);
				GLES20.glDisable(GLES20.GL_BLEND);
				GLES20.glDepthMask(true);
			}

			material.useProgram();
			material.bindTextures();
			material.setTextures(textureBuffer, false);
			material.setNormals(normalBuffer);
			material.setColors(colorBuffer);
			material.setCamera(camera);
			material.setVertices(vertexBuffer);
			material.setClip(new float[]{10f,10f,10f,0f});

			GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

			material.setLight(light);

		}
		float[] modelMatrix = new float[16];
		float[] scaleMatrix = new float[16];
		float[] rotateMatrix = new float[16];
		float[] matrix = new float[16];
		float[] tmpMatrix = new float[16];

		Matrix.setIdentityM(modelMatrix, 0);
		Matrix.setIdentityM(scaleMatrix, 0);
		Matrix.scaleM(scaleMatrix, 0, scale.x, scale.y, scale.z);

		Matrix.setIdentityM(rotateMatrix, 0);

		if(camera.getAt() == null) {
			camera.rotate(rotateMatrix, 0, rotation.x, 1.0f, 0.0f, 0.0f);
			camera.rotate(rotateMatrix, 0, rotation.y, 0.0f, 1.0f, 0.0f);
			camera.rotate(rotateMatrix, 0, rotation.z, 0.0f, 0.0f, 1.0f);
			rotation.set(new float[]{0,0,0});
		} else 
			Matrix.setLookAtM(rotateMatrix, 0, 0, 0, 0, camera.getAt().x, camera.getAt().y, camera.getAt().z, 0, 1f, 0);

		Matrix.translateM(modelMatrix, 0, -position.x, position.y, position.z);
		Matrix.setIdentityM(matrix, 0);
		Matrix.multiplyMM(matrix, 0, modelMatrix, 0, scaleMatrix, 0);
		Matrix.multiplyMM(tmpMatrix, 0, rotateMatrix, 0, accRotation, 0);
		System.arraycopy(tmpMatrix, 0, accRotation, 0, 16);
		Matrix.multiplyMM(modelMatrix, 0, matrix, 0, accRotation, 0);

		if (parentMatrix != null) {
			Matrix.multiplyMM(matrix, 0, parentMatrix, 0, modelMatrix, 0);
			System.arraycopy(matrix, 0, modelMatrix, 0, 16);
		}
		Matrix.multiplyMM(modelViewMatrix, 0, camera.getViewMatrix(), 0, modelMatrix, 0);
		Matrix.multiplyMM(modelViewMatrix, 0, camera.getProjectionMatrix(), 0, modelViewMatrix, 0);

		if(!touch){
			material.setModelViewProjectionMatrix(modelViewMatrix);
			material.setModelViewMatrix(modelMatrix);
			material.setViewMatrix(camera.getViewMatrix());

			indexBuffer.position(0);
			GLES20.glDrawElements(GL_Mode, indexSize, GLES20.GL_UNSIGNED_SHORT, indexBuffer);

			GLES20.glDisable(GLES20.GL_CULL_FACE);
			GLES20.glDisable(GLES20.GL_BLEND);
			GLES20.glDisable(GLES20.GL_DEPTH_TEST);
		}
		for (int i = 0; i < objectNodes.size(); ++i) {
			objectNodes.get(i).render(camera,modelMatrix);
		}
	}

	public void setScreenCoordinates(float x, float y, int viewportWidth, int viewportHeight, float eyeZ) {
		float[] r1 = new float[16];
		int[] viewport = new int[] { 0, 0, viewportWidth, viewportHeight };
		float[] modelMatrix = new float[16];
		Matrix.setIdentityM(modelMatrix, 0);
		GLU.gluUnProject(x, viewportHeight - y, 0.0f, modelMatrix, 0, projectionMatrix, 0, viewport, 0, r1, 0);
		position = new Point3D(r1[0] * eyeZ, r1[1] * -eyeZ, 0);
	}

	public void setColor(float r, float g, float b, float a) {
		if(colorBuffer == null || colorBuffer.capacity() == 0)
			colorBuffer = ByteBuffer.allocateDirect(VertexSize * 16).order(ByteOrder.nativeOrder()).asFloatBuffer();
		colorBuffer.position(0);

		while(colorBuffer.remaining() > 3) {
			colorBuffer.put(r);
			colorBuffer.put(g);
			colorBuffer.put(b);
			colorBuffer.put(a);
		}
		colorBuffer.position(0);
	}

	public void setColor(int color) {
		setColor(Color.red(color) / 255f, Color.green(color) / 255f,
				Color.blue(color) / 255f, Color.alpha(color) / 255f);
	}

	public FloatBuffer getVertexBuffer() {
		return vertexBuffer;
	}

	public void setVertexBuffer(FloatBuffer vertexBuffer) {
		this.vertexBuffer = vertexBuffer;
	}

	public FloatBuffer getNormalBuffer() {
		return normalBuffer;
	}

	public void setNormalBuffer(FloatBuffer normalBuffer) {
		this.normalBuffer = normalBuffer;
	}

	public FloatBuffer getColorBuffer() {
		return colorBuffer;
	}

	public void setColorBuffer(FloatBuffer colorBuffer) {
		this.colorBuffer = colorBuffer;
	}

	public ShortBuffer getIndexBuffer() {
		return indexBuffer;
	}

	public void setIndexBuffer(ShortBuffer indexBuffer) {
		this.indexBuffer = indexBuffer;
	}

	public ArrayList<Object3D> getObjectNodes() {
		return objectNodes;
	}

	public void setObjectNodes(ArrayList<Object3D> objectNodes) {
		this.objectNodes = objectNodes;
	}

	public Point3D getPosition() {
		return position;
	}

	public void setPosition(Point3D position) {
		this.position = position;
	}

	public boolean isVisible() {
		return visible;
	}

	public void setVisible(boolean visible) {
		this.visible = visible;
	}

	public int getGL_Mode() {
		return GL_Mode;
	}

	public void setGL_Mode(int gL_Mode) {
		GL_Mode = gL_Mode;
	}

	public String getName() {
		return name;
	}

	public Point3D getRotation() {
		return rotation;
	}

	public void setRotation(Point3D rotation) {
		this.rotation = rotation;
	}

	public Point3D getScale() {
		return scale;
	}

	public void setScale(Point3D scale) {
		this.scale = scale;
	}

	public Material getMaterial() {
		return material;
	}

	public void setMaterial(Material material) {
		this.material = material;
	}

	public Light getLight() {
		return light;
	}

	public void setLight(Light light) {
		this.light = light;
		for(int i=0; i<objectNodes.size(); ++i) 
			objectNodes.get(i).setLight(light);
	}

	public boolean isTransparent() {
		return transparent;
	}

	public void setTransparent(boolean transparent) {
		this.transparent = transparent;
	}

	public float[] getModelViewMatrix() {
		return modelViewMatrix;
	}

	public void setModelViewMatrix(float[] modelViewMatrix) {
		this.modelViewMatrix = modelViewMatrix;
	}

	public Object3DSerialized serialize(){
		return new Object3DSerialized(vertexBuffer,normalBuffer,colorBuffer,textureBuffer, indexBuffer,scale, position, rotation, material, transparent, objectNodes,visible,light,accRotation,touch);
	}
	@Override
	public int hashCode() {
		int hash = 0;
		for (Object3D o : getObjectNodes())
			hash += o.hashCode();
		return hash + vertexBuffer.hashCode() + normalBuffer.hashCode() + colorBuffer.hashCode() + textureBuffer.hashCode() + indexBuffer.hashCode() 
				+ scale.hashCode() + rotation.hashCode() + position.hashCode() + (transparent ? 0 : 1) + (visible ? 0 : 1) + light.hashCode() + accRotation.hashCode() + (touch ? 0: 1);


	}
	public boolean isTouch(){
		return touch;
	}
	public void setTouch(boolean t){
		touch = t;
	}

	public void setToInit() {
		accRotation = accRotationInit;
		position.set(initPoint);
	}
}
